package dkbta.ontology.elements;

import static dkbta.util.Utils.isEmpty;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Set;

import dkbta.ontology.Validatable;
import dkbta.ontology.values.ScaledValues;
import dkbta.ontology.values.Values;
import dkbta.ontology.values.ValuesType;
import dkbta.processing.Patient;
import dkbta.processing.Patients;
import dkbta.processing.WorkQueue;
import dkbta.processing.tasks.Task;
import dkbta.util.Pair;

/**
 * Represents a general ontology element and contains the common components
 * 
 * @author Uri Kanonov
 */
public abstract class ElementDef implements Validatable{
	/** The element's type */
	protected final ElementType _type;

	/** The name of the element */
	protected final String _name;

	/** The component type of the element */
	protected final String _componentType;

	/** The possible values of the element */
	protected final ScaledValues _scaledValues;

	/** The element's hashcode, for later faster hashing */
	private final int _hashCode;

	/**
	 * The elements the computation of this element directly depends on. For example for a
	 * state this would include the necessary contexts and the abstracted-from elements
	 */
	protected final Collection<ElementDef> _prerequisiteElements;

	/**
	 * Initializes an ontology element definition
	 * 
	 * @param elementType The element's type
	 * @param name The element's name
	 * @param componentType The element's component type
	 * @param possibleValues The possible values of the element
	 */
	public ElementDef(ElementType elementType, String name, String componentType,
		ScaledValues possibleValues){
		_type = elementType;
		_scaledValues = possibleValues;
		_componentType = isEmpty(componentType) ? null : componentType;
		_name = name.intern();

		_prerequisiteElements = new ArrayList<ElementDef>();

		_hashCode = (_type + _name).hashCode();
	}

	/**
	 * Returns the name of the element
	 * 
	 * @return The name of the element
	 */
	public final String getName(){
		return _name;
	}

	/**
	 * Returns the component type of the element
	 * 
	 * @return The component type of the element
	 */
	public final String getComponentType(){
		return _componentType;
	}

	/**
	 * Returns a complete (all of its fields) string representation of the element
	 * 
	 * @return A complete (all of its fields) string representation of the element
	 */
	public String completeToString(){
		StringBuilder sb = new StringBuilder();
		sb.append("--- ").append(getElementType()).append(": ").append(_name).append(
			" ---\r\n");
		if (_componentType != null){
			sb.append("Component Type: ").append(_componentType).append("\r\n");
		}
		if (_scaledValues != null){
			sb.append("Possible Values: ").append(_scaledValues).append("\r\n");
		}

		return sb.toString();
	}

	/**
	 * Returns a short string representation of the element
	 * 
	 * @return A short string representation of the element
	 */
	@Override
	public final String toString(){
		return _type + ": " + _name;
	}

	/**
	 * Returns the element instance type of this instance
	 * 
	 * @return The element instance type of this instance
	 */
	public final ElementType getElementType(){
		return _type;
	}

	/**
	 * Checks this element and another one for equality based on their name and element
	 * type
	 * 
	 * @param other The element to be compared to
	 * @return Whether this element is equal to other
	 */
	@Override
	public boolean equals(Object other){
		if (other instanceof ElementDef){
			ElementDef o = (ElementDef)other;
			return _name.equals(o._name) && _type == o._type;
		}
		return false;
	}

	/**
	 * Returns a hash code value for the element
	 * 
	 * @return A hash code value for the element
	 */
	@Override
	public final int hashCode(){
		return _hashCode;
	}

	/**
	 * Returns the scaled values the element can have
	 * 
	 * @return The scaled values the element can have
	 */
	public final ScaledValues getScaledValues(){
		return _scaledValues;
	}

	/**
	 * Returns the possible values the element can have
	 * 
	 * @return The possible values the element can have
	 */
	public final Values getValues(){
		return _scaledValues == null ? null : _scaledValues.getValues();
	}

	/**
	 * Returns the type of the values the element can have
	 * 
	 * @return The type of the values the element can have
	 */
	public final ValuesType getValueType(){
		if (_scaledValues == null){
			return null;
		}
		return _scaledValues.getValueType();
	}

	/**
	 * Creates a new instance of this element with the given raw data
	 * 
	 * @param startTime The start time
	 * @param endTime The end time
	 * @param values The potential value(s)
	 * @return The new instance constructed from the raw data
	 */
	public abstract Element newInstance(long startTime, long endTime, String[] values);

	/**
	 * This method first checks for if all of the prerequisite elements for this element
	 * have been computed (creating computation tasks along the way if need be). If so it
	 * proceeds to the actual computation, otherwise it returns false (in such a case the
	 * method will be called again once all prerequisite elements are ready and then it
	 * will proceed to the actual computation)
	 * 
	 * @param patient The associated patient
	 * @param wq A reference to the work queue
	 * @param parentTask The task whose associated element is this element
	 * @return Whether the instances of this element have been computed (happens only if
	 *         all prerequisite elements are ready)
	 */
	public final boolean compute(Patient patient, WorkQueue wq, Task parentTask){
		boolean prerequisitesComputed = true;
		for (ElementDef ed : _prerequisiteElements){
			if (!patient.hasBeenComputed(ed, wq, parentTask)){
				prerequisitesComputed = false;
			}
		}

		// If all elements are ready, we can compute the instances of this element
		if (prerequisitesComputed){
			compute(patient);
			return true;
		}else{
			return false;
		}
	}

	/**
	 * This method first checks for if all of the prerequisite elements for this element
	 * have been computed (creating computation tasks along the way if need be). If so it
	 * proceeds to the actual computation, otherwise it returns false (in such a case the
	 * method will be called again once all prerequisite elements are ready and then it
	 * will proceed to the actual computation)
	 * 
	 * @param patients All of the patients
	 * @param patientIds The IDs of the associated patients
	 * @param wq A reference to the work queue
	 * @param parentTask The task whose associated element is this element
	 * @return Whether the instances of this element have been computed (happens only if
	 *         all prerequisite elements are ready)
	 */
	public final boolean compute(Patients patients, Pair<Integer, Integer>[] patientIds,
		WorkQueue wq, Task parentTask){
		boolean prerequisitesComputed = true;
		Iterator<Patient> iter = patients.rangeIterator(patientIds);
		while (iter.hasNext()){
			Patient p = iter.next();
			for (ElementDef ed : _prerequisiteElements){
				if (!p.hasBeenComputed(ed, wq, parentTask)){
					prerequisitesComputed = false;
				}
			}
		}

		// If all elements are ready, we can compute the instances of this element
		if (prerequisitesComputed){
			compute(patients, patientIds);
			return true;
		}else{
			return false;
		}
	}

	/**
	 * Computes the instances of the element for a single patient.<br>
	 * <br>
	 * <b>Important:</b> The standard elements are computed for a single patient so the
	 * implementation of this method is left to the extending class. If the element is
	 * supposed to be computed for a single patient, then override this method and provide
	 * a proper implementation.
	 * 
	 * @param p The single patient
	 * @throws UnsupportedOperationException If the element is supposed to be computed for
	 *         mutliple patients
	 */
	protected abstract void compute(Patient p) throws UnsupportedOperationException;

	/**
	 * Computes the instances of the element for multiple patients. <br>
	 * <br>
	 * <b>Important:</b> The standard elements are computed for a single patient so the
	 * standard implementation of this method throws an Exception. If the element is
	 * supposed to be computed for multiple patients, then override this method and
	 * provide a proper implementation. It would be also recommended to override
	 * compute(Patient p) and to throw an exception.
	 * 
	 * @param patients All of the patients
	 * @param patientIds The IDs of the associated patients
	 * @throws UnsupportedOperationException If the element is supposed to be computed for
	 *         a single patient
	 */
	protected void compute(Patients patients, Pair<Integer, Integer>[] patientIds)
			throws UnsupportedOperationException{
		throw new UnsupportedOperationException(
				_type
						+ "s are computed for a single patient, invoke compute(Patient p) instead");
	}

	public void addRequiredPrimsAndEvents(Set<ElementDef> requiredPrimsAndEvents){
		for (ElementDef ed : _prerequisiteElements){
			ed.addRequiredPrimsAndEvents(requiredPrimsAndEvents);
		}
	}

	public final Collection<ElementDef> getPrerequisites(){
		return Collections.unmodifiableCollection(_prerequisiteElements);
	}
}
